/*
 * RARoScope - Java Library for Scanning RAR Archives
 * 
 * Copyright 2008 Adarsh Ramamurthy 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0 
 *  
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 * 
 * Homepage: http://www.adarshr.com/papers/raroscope
 */

package com.adarshr.raroscope;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.NoSuchElementException;

/**
 * Represents a RAR archive.
 * <p/>
 * This class is used to enumerate the entries in a RAR file.
 *
 * @author Adarsh Ramamurthy
 * @version 1.0, 10th March 2008
 */
public class RARFile {
    /**
     * The underlying stream.
     */
    private InputStream stream;
    static final byte[] RAR_ID_BYTE = {82, 97, 114, 33, 26, 7, 0};

    static final String RAR_ID = new String(RAR_ID_BYTE, Charset.forName("ASCII"));
    /**
     * Marker block of RAR archives. It is "Rar!" in reverse bytes.
     * Rar!\u001A\u0007\u0000
     */
    private static final long MARKER = 0x21726152L;

    /**
     * To hold the available bytes in the stream.
     */
    private long available;

    /**
     * Constructs an instance of <tt>RARFile</tt> for performing operations
     * on the archive.
     *
     * @param name the RAR file name.
     * @throws IOException in case of errors reading from the archive.
     */
    public RARFile(String name) throws IOException {
        this(new File(name));
    }

    /**
     * Constructs an instance of <tt>RARFile</tt> for performing operations
     * on the archive.
     *
     * @param file the RAR file.
     * @throws IOException in case of errors reading from the archive.
     */
    public RARFile(File file) throws IOException {
        this.stream = new FileInputStream(file);

        this.available = this.stream.available();
        //min header length 7
        byte[] headers = new byte[7 + 13];

        // Read the Marker Block and Archive Header Blocks.
        this.stream.read(headers);

        if (MARKER != getLong(headers, 0, 3)) {
            throw new IOException("Invalid RAR archive");
        }
    }

    /**
     * RAR_ID = bytes("Rar!\x1a\x07\x00", 'ascii')
     * ZERO = bytes("\0", 'ascii')
     * EMPTY = bytes("", 'ascii')
     * Enumerates all the entries in the RAR archive.
     *
     * @return an instance of <tt>Enumeration</tt> for displaying the entries.
     * @see RAREntry
     */
    public Enumeration<RAREntry> entries() {
        return new Enumeration<RAREntry>() {

            public RAREntry nextElement() {
                byte[] buf = new byte[32];

                RAREntry entry = null;

                try {
                    RARFile.this.available -= RARFile.this.stream.read(buf);

                    int type = buf[2] & 0xFF;

                    // Means File Header Block
                    if (type == 0x74) {
                        entry = new RAREntry();

                        long flags = getLong(buf, 3, 4);

                        entry.setDirectory((flags & 0xE0) == 0xE0);

                        long pSize = getLong(buf, 7, 10);
                        long size = getLong(buf, 11, 14);

                        // Very large file ( > 2GB )
                        if ((flags & 0x100) == 0x100) {
                            byte[] hiBytes = new byte[8];

                            RARFile.this.available -=
                                RARFile.this.stream.read(hiBytes);

                            pSize = getLong(hiBytes, 0, 4) << 32 | pSize;
                            size = getLong(hiBytes, 5, 8) << 32 | size;
                        }

                        long hSize = getLong(buf, 5, 6);

                        entry.setCompressedSize(pSize);
                        entry.setSize(getLong(buf, 11, 14));
                        entry.setHostOS(toOS(buf[15] & 0xFF));
                        entry.setCrc(getLong(buf, 16, 19));
                        entry.setTime(toDate(getLong(buf, 20, 23)));
                        entry.setVersion(toVersion(buf[24] & 0xFF));
                        entry.setMethod(toMethod(buf[25] & 0xFF));
                        entry.setEncrypted((flags & 0x04) != 0);
                        long nSize = getLong(buf, 26, 27);

                        byte[] name = new byte[(int) nSize];

                        RARFile.this.available -=
                                RARFile.this.stream.read(name);
                        if (isUnicode(flags)) {

                            int length = 0;

                            String fileNameW = "";
                            while (length < name.length
                                    && name[length] != 0) {
                                length++;
                            }
                            byte[] namex = new byte[length];
                            System.arraycopy(name, 0, namex, 0, namex.length);
                            //String fileName = new String(name);
                            if (length != nSize) {
                                length++;
                                fileNameW = decode(name, length);
                            }
                            entry.setName(fileNameW);
                        } else {
                            entry.setName(new String(name));
                        }
                        RARFile.this.available -=
                                RARFile.this.stream.skip(
                                        hSize - (32 + nSize) + pSize);
                    }
                } catch (IOException e) {
                    throw new NoSuchElementException(e.getMessage());
                }

                if (entry == null) {
                    throw new NoSuchElementException();
                }

                return entry;
            }

            public boolean hasMoreElements() {
                return RARFile.this.available > 32;
            }

        };
    }

    /**
     * Closes the archive.
     *
     * @throws IOException in case of errors while closing.
     */
    public void close() throws IOException {
        this.stream.close();
    }

    /**
     * Converts the input inverted array of bytes to a long representation.
     *
     * @param bytes the byte array to be converted.
     * @return the long value.
     */
    protected long getLong(byte[] bytes) {
        long ret = 0;
        long mask = 0;

        for (int i = 0; i < bytes.length; i++) {
            ret |= (bytes[i] & 0xFF) << (8 * i); // mask and shift left
            mask = (mask << 8) | 0xFF; // generate the final mask
        }

        return ret & mask;
    }

    /**
     * Converts the input inverted array of bytes to a long representation.
     * Conversion is done inclusive of both the limits specified.
     *
     * @param bytes the byte array to be converted.
     * @param start the index to start with.
     * @param end   the end index.
     * @return the long value.
     */
    protected long getLong(byte[] bytes, int start, int end) {
        long ret = 0;
        long mask = 0;

        if (start < 0 || end >= bytes.length) {
            return ret;
        }

        for (int i = start, j = 0; i <= end; i++, j++) {
            ret |= (bytes[i] & 0xFF) << (8 * j); // mask and shift left
            mask = (mask << 8) | 0xFF; // generate the final mask
        }

        return ret & mask;
    }

    /**
     * Converts the DOS time to Java date.
     *
     * @param dosTime MS DOS format time.
     * @return an instance of <tt>Date</tt>.
     */
    protected Date toDate(long dosTime) {
        Calendar calendar = Calendar.getInstance();

        calendar.set(
            (int) (((dosTime >> 25) & 0x7f) + 1980),  // year
            (int) (((dosTime >> 21) & 0x0f) - 1),  // month
            (int) ((dosTime >> 16) & 0x1f),  // date
            (int) ((dosTime >> 11) & 0x1f),  // hours
            (int) ((dosTime >> 5) & 0x3f),  // minutes
            (int) ((dosTime << 1) & 0x3e));  // seconds

        return calendar.getTime();
    }

    /**
     * Translate the OS byte to a human readable string.
     *
     * @param o the number to be translated (1 byte).
     * @return the OS string.
     */
    protected String toOS(int o) {
        String os = null;

        switch (o) {
            case 0:
                os = "MS DOS";
                break;

            case 1:
                os = "OS/2";
                break;

            case 2:
                os = "Win32";
                break;

            case 3:
                os = "Unix";
                break;

            case 4:
                os = "Mac OS";
                break;

            case 5:
                os = "BeOS";
                break;

            default:
                os = "Unknown";
                break;
        }

        return os;
    }

    /**
     * Decodes the version information. Version number will be of the format
     * 10 * Major Version + Minor Version.
     *
     * @param v the version number.
     * @return the decoded version in the format "major.minor".
     */
    protected String toVersion(int v) {
        return String.valueOf(v / 10F);
    }

    /**
     * Translates the compression method into a string.
     *
     * @param m the compression method number.
     * @return the compression method string.
     */
    protected String toMethod(int m) {
        String method = null;

        switch (m) {
            case 0x30:
                method = "Storing";
                break;

            case 0x31:
                method = "Fastest Compression";
                break;

            case 0x32:
                method = "Fast Compression";
                break;

            case 0x33:
                method = "Normal Compression";
                break;

            case 0x34:
                method = "Good Compression";
                break;

            case 0x35:
                method = "Best Compression";
                break;

            default:
                method = "Unknown";
                break;
        }

        return method;
    }

    public static int getChar(byte[] name, int pos) {
        return name[pos] & 0xff;
    }

    public static String decode(byte[] name, int encPos) {
        int decPos = 0;
        int flags = 0;
        int flagBits = 0;

        int low = 0;
        int high = 0;
        int highByte = getChar(name, encPos++);
        StringBuffer buf = new StringBuffer();
        while (encPos < name.length) {
            if (flagBits == 0) {
                flags = getChar(name, encPos++);
                flagBits = 8;
            }
            switch (flags >> 6) {
                case 0:
                    buf.append((char) (getChar(name, encPos++)));
                    ++decPos;
                    break;
                case 1:
                    buf.append((char) (getChar(name, encPos++) + (highByte << 8)));
                    ++decPos;
                    break;
                case 2:
                    low = getChar(name, encPos);
                    high = getChar(name, encPos + 1);
                    buf.append((char) ((high << 8) + low));
                    ++decPos;
                    encPos += 2;
                    break;
                case 3:
                    int length = getChar(name, encPos++);
                    if ((length & 0x80) != 0) {
                        int correction = getChar(name, encPos++);
                        for (length = (length & 0x7f) + 2; length > 0 && decPos < name.length; length--, decPos++) {
                            low = (getChar(name, decPos) + correction) & 0xff;
                            buf.append((char) ((highByte << 8) + low));
                        }
                    } else {
                        for (length += 2; length > 0 && decPos < name.length; length--, decPos++) {
                            buf.append((char) (getChar(name, decPos)));
                        }
                    }
                    break;
            }
            flags = (flags << 2) & 0xff;
            flagBits -= 2;
        }
        return buf.toString();
    }

    /**
     * #@+
     * RAR 1.5 - 4.x archive format values (thanks to Marko Kreen)
     */
    // block types
    public static final int RAR_BLOCK_MARK = 0x72;// # r     # Marker block (must be the first block)
    public static final int RAR_BLOCK_MAIN = 0x73;// # s   # Archive header
    public static final int RAR_BLOCK_FILE = 0x74;// # t    # File header
    public static final int RAR_BLOCK_OLD_COMMENT = 0x75;//# u
    public static final int RAR_BLOCK_OLD_EXTRA = 0x76;//# v # AV_HEAD - Extra info block
    public static final int RAR_BLOCK_OLD_SUB = 0x77;//# w # OS/2 extended attributes subblock
    public static final int RAR_BLOCK_OLD_RECOVERY = 0x78;// # x # old-style recovery record
    public static final int RAR_BLOCK_OLD_AUTH = 0x79;//# y   # SIGN_HEAD
    public static final int RAR_BLOCK_SUB = 0x7a;//# z  # subblock (contains new-style RR, CMT, AV)
    public static final int RAR_BLOCK_ENDARC = 0x7b;// # {   # Archive end (optional: -en switch rar)
    public static final int RAR_BLOCK_NULL = 0x00;
    //flags for RAR_BLOCK_MAIN
    public static final int RAR_MAIN_VOLUME = 0x0001;
    public static final int RAR_MAIN_COMMENT = 0x0002;
    public static final int RAR_MAIN_LOCK = 0x0004;
    public static final int RAR_MAIN_SOLID = 0x0008;
    public static final int RAR_MAIN_NEWNUMBERING = 0x0010;
    public static final int RAR_MAIN_AUTH = 0x0020;
    public static final int RAR_MAIN_RECOVERY = 0x0040;
    public static final int RAR_MAIN_PASSWORD = 0x0080;
    public static final int RAR_MAIN_FIRSTVOLUME = 0x0100;
    public static final int RAR_MAIN_ENCRYPTVER = 0x0200;
    //flags for RAR_BLOCK_FILE
    public static final int RAR_FILE_SPLIT_BEFORE = 0x0001;
    public static final int RAR_FILE_SPLIT_AFTER = 0x0002;
    public static final int RAR_FILE_PASSWORD = 0x0004;
    public static final int RAR_FILE_COMMENT = 0x0008;
    public static final int RAR_FILE_SOLID = 0x0010;
    public static final int RAR_FILE_DICTMASK = 0x00e0;
    public static final int RAR_FILE_DICT64 = 0x0000;
    public static final int RAR_FILE_DICT128 = 0x0020;
    public static final int RAR_FILE_DICT256 = 0x0040;
    public static final int RAR_FILE_DICT512 = 0x0060;
    public static final int RAR_FILE_DICT1024 = 0x0080;
    public static final int RAR_FILE_DICT2048 = 0x00a0;
    public static final int RAR_FILE_DICT4096 = 0x00c0;
    public static final int RAR_FILE_DIRECTORY = 0x00e0;
    public static final int RAR_FILE_LARGE = 0x0100;
    public static final int RAR_FILE_UNICODE = 0x0200;
    public static final int RAR_FILE_SALT = 0x0400;
    public static final int RAR_FILE_VERSION = 0x0800;
    public static final int RAR_FILE_EXTTIME = 0x1000;
    public static final int RAR_FILE_EXTFLAGS = 0x2000;
    //flags for RAR_BLOCK_ENDARC
    public static final int RAR_ENDARC_NEXT_VOLUME = 0x0001;
    public static final int RAR_ENDARC_DATACRC = 0x0002;
    public static final int RAR_ENDARC_REVSPACE = 0x0004;
    public static final int RAR_ENDARC_VOLNR = 0x0008;

    //flags common to all blocks
    public static final int RAR_SKIP_IF_UNKNOWN = 0x4000;
    public static final int RAR_LONG_BLOCK = 0x8000;

    public boolean isUnicode(long flags) {
        return (flags & RAR_FILE_UNICODE) != 0;
    }

    /**
     * RAR archive format version.
     */
    public static final String FMT_RAR14 = "1.4";
    public static final String FMT_RAR15 = "1.5";
    public static final String FMT_RAR20 = "2.x";// "20: RAR 2.x compression",
    //            26: "26: files larger than 2GB",
//            29: "29: RAR 3.x compression",
//            36: "36: alternative hash",
    public static final String FMT_RAR50 = "5.0";


    /**
     * Signature for the RAR Marker block.
     *
     * @var string
     */
    protected String markerBlock = "\\x52\\x61\\x72\\x21\\x1a\\x07\\x00";

    /**
     * Signature for RAR 5.0 format archives.
     *
     * @var string
     */
    protected String markerRar50 = "\\x52\\x61\\x72\\x21\\x1a\\x07\\x01\\x00";
    /**
     * #@+
     * RAR 5.0 archive format values
     */

    // Block types
    public static final int R50_BLOCK_MAIN = 0x01;
    public static final int R50_BLOCK_FILE = 0x02;
    public static final int R50_BLOCK_SERVICE = 0x03;
    public static final int R50_BLOCK_CRYPT = 0x04;
    public static final int R50_BLOCK_ENDARC = 0x05;

    // Flags for all block types
    public static final int R50_HAS_EXTRA = 0x0001;
    public static final int R50_HAS_DATA = 0x0002;
    public static final int R50_SKIP_IF_UNKNOWN = 0x0004;
    public static final int R50_SPLIT_BEFORE = 0x0008;
    public static final int R50_SPLIT_AFTER = 0x0010;
    public static final int R50_IS_CHILD = 0x0020;
    public static final int R50_INHERITED = 0x0040;

    // Service block types
    public static final String R50_SERVICE_COMMENT = "CMT";
    public static final String R50_SERVICE_QUICKOPEN = "QO";
    public static final String R50_SERVICE_ACL = "ACL";
    public static final String R50_SERVICE_STREAM = "STM";
    public static final String R50_SERVICE_RECOVERY = "RR";

    // Subtypes for BLOCK_SUB
    public static final String SUBTYPE_UOWNER = "UOW";
    public static final String SUBTYPE_AUTHVER = "AV";
    public static final String SUBTYPE_OS2EA = "EA2";
    public static final String SUBTYPE_BEOSEA = "EABE";

    // Flags for R50_BLOCK_MAIN
    public static final int R50_MAIN_VOLUME = 0x0001;
    public static final int R50_MAIN_VOLNUMBER = 0x0002;
    public static final int R50_MAIN_SOLID = 0x0004;
    public static final int R50_MAIN_RECOVERY = 0x0008;
    public static final int R50_MAIN_LOCK = 0x0010;

    // Flags for R50_BLOCK_FILE
    public static final int R50_FILE_DIRECTORY = 0x0001;
    public static final int R50_FILE_UTIME = 0x0002;
    public static final int R50_FILE_CRC32 = 0x0004;
    public static final int R50_FILE_UNPUNKNOWN = 0x0008;

    // Flags for R50_BLOCK_ENDARC
    public static final int R50_ENDARC_NEXT_VOLUME = 0x0001;

    // Extra record types for R50_BLOCK_MAIN
    public static final int R50_MEXTRA_LOCATOR = 0x01;

    // Flags for R50_MEXTRA_LOCATOR
    public static final int R50_MEXTRA_LOC_QLIST = 0x0001;
    public static final int R50_MEXTRA_LOC_RR = 0x0002;

    // Extra record types for R50_BLOCK_FILE
    public static final int R50_FEXTRA_CRYPT = 0x01;
    public static final int R50_FEXTRA_HASH = 0x02;
    public static final int R50_FEXTRA_HTIME = 0x03;
    public static final int R50_FEXTRA_VERSION = 0x04;
    public static final int R50_FEXTRA_REDIR = 0x05;
    public static final int R50_FEXTRA_UOWNER = 0x06;
    public static final int R50_FEXTRA_SUBDATA = 0x07;

    // Flags for R50_FEXTRA_HTIME
    public static final int R50_FEXTRA_HT_UNIX = 0x0001;
    public static final int R50_FEXTRA_HT_MTIME = 0x0002;
    public static final int R50_FEXTRA_HT_CTIME = 0x0004;
    public static final int R50_FEXTRA_HT_ATIME = 0x0008;

    // Compression methods
    public static final int R50_METHOD_STORE = 0;
    public static final int R50_METHOD_FASTEST = 1;
    public static final int R50_METHOD_FAST = 2;
    public static final int R50_METHOD_NORMAL = 3;
    public static final int R50_METHOD_GOOD = 4;
    public static final int R50_METHOD_BEST = 5;

    // OS types
    public static final int R50_OS_WIN32 = 0;
    public static final int R50_OS_UNIX = 1;


}
